package container

import (
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"syscall"

	"gitee.com/chuanjinge/go-container/config"
	"gitee.com/chuanjinge/go-container/container/overlay2"
	"gitee.com/chuanjinge/go-container/pkg/loggers"
)

// 容器根目录的切换逻辑
// 将当前文件系统命名空间(syscall.NEWNS)的根目录切换到指定目录
// -- 实际是调用 pivot_root 这个系统调用来实现
// @param string rootPath 指定的新的根目录
func pivotRoot(rootPath string) error {

	// 由于pivot_root()要求新老root必须处于不同的文件系统，所以得把rootPath这个当前处于 / 挂载点下的目录重新挂载一下，形成新的挂载点
	// 指定参数三fstype可以为空或者为bind，表示是执行绑定挂载，也就是把一个已经存在的目录或者挂载点镜像到另一个地方
	// -- man 2 mount 查看系统调用的参数及说明--golang封装的都是系统调用
	// -- man 1 mount 查看shell命令的参数及说明
	// ---- MS_BIND 表示的是挂载绑定
	// ---- MS_REC  表示的是子目录中的那些挂载点也执行同样的操作
	if err := syscall.Mount(rootPath, rootPath, "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
		return err
	}

	// 在 rootPath下创建 old_root 目录存储作为原有根目录的挂载点，为了待会把原有根目录给卸载掉
	oldRootMountPoint := filepath.Join(rootPath, "old_root")
	if _, err := os.Stat(oldRootMountPoint); err != nil && os.IsNotExist(err) {
		if err := os.Mkdir(oldRootMountPoint, 0777); err != nil {
			return err
		}
	}

	// 执行pivot_root这个系统调用
	if err := syscall.PivotRoot(rootPath, oldRootMountPoint); err != nil {
		return errors.Join(err, fmt.Errorf("pivot_root Fail, new_root:%v;old_root:%v", rootPath, oldRootMountPoint))
	}

	// 在新的根目录下开展工作，当前命名空间下的根目录实际上就是老的rootPath
	if err := os.Chdir("/"); err != nil {
		return errors.Join(err, fmt.Errorf("new namespace after pivotroot chdir Fail"))
	}

	// 最后把老的root给umount掉
	oldRootMountPointNow := filepath.Join("/", "old_root")
	if err := syscall.Unmount(oldRootMountPointNow, syscall.MNT_DETACH); err != nil {
		return errors.Join(err, fmt.Errorf("umount old_root fail"))
	}
	// 删除掉挂载点文件夹
	return os.Remove(oldRootMountPointNow)
}

// 准备所有的挂载项
// 包括根目录文件系统、proc、tmpfs
// @param string containerId 容器ID
// @param []string volumeList 挂载卷的映射信息 ["host/path/A1:container/path/A2", ""]
func PrepareUfs(containerId string, volumeList []string) error {
	// 基于overlay2这种UFS进行初始文件系统的搭建，在该目录下搞："/data/tinydocker/container"
	// 在该目录下生成 merged/lower/upper/work 目录用来存储各个层的镜像
	ufsDir := overlay2.NewUfsDir(containerId)

	// 后期应该改为依赖非标识符参数
	//image := "/data/tinydocker/image/busybox"
	//image := "/data/tinydocker/image/myfs"
	image := path.Join(config.GlobalConfig().ImagePath, "myfs")
	if err := ufsDir.ReleaseBaseImageFromLocal(image); err != nil {
		return err
	}
	// 挂载为overlay的文件系统
	loggers.GetLoggers().LoggerRun.Println("realproc:挂载为overlay的文件系统")
	if err := ufsDir.MountOverlayFs(); err != nil {
		return err
	}
	// 看看有没有挂载目录，一并挂载上
	loggers.GetLoggers().LoggerRun.Println("realproc:挂载share目录")
	if err := ufsDir.ProcessSharedVolume(volumeList); err != nil {
		return err
	}

	// systemd 加入linux之后, mount namespace 就变成 shared by default, 所以你必须显示
	// 声明你要这个新的mount namespace独立。
	// 如果不先做 private mount，会导致挂载事件外泄，后续执行 pivotRoot 会出现 invalid argument 错误
	_ = syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "")
	loggers.GetLoggers().LoggerRun.Println("realproc:pivotroot切换当前命名空间的根目录", ufsDir.MergedDir)
	if err := pivotRoot(ufsDir.MergedDir); err != nil {
		return errors.Join(err, fmt.Errorf("切换根目录出错"))
	}

	/*
		proc系统的挂载
			挂载标志：
				syscall.MS_NOEXEC：禁止执行二进制文件。这意味着 /proc 中的任何文件都不能被执行。
				syscall.MS_NOSUID：忽略设置用户 ID 和组 ID 的特殊权限位。这防止了 /proc 中的文件利用 SUID 或 SGID 权限提升权限。
				syscall.MS_NODEV：不允许访问设备文件。这意味着 /proc 中的文件不能被视为设备节点。
		// 如果不先做 private mount，会导致挂载事件外泄，后续再执行 mydocker 命令时 /proc 文件系统异常
		// 临时解决的话可以执行 mount -t proc proc /proc 命令重新挂载来解决
	*/
	loggers.GetLoggers().LoggerRun.Println("realproc:挂载proc目录")
	defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
	_ = syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")

	// tmpfs 系统的挂载
	// 不挂载 /dev，会导致容器内部无法访问和使用许多设备，这可能导致系统无法正常工作
	syscall.Mount("tmpfs", "/dev", "tmpfs", syscall.MS_NOSUID, "mode=755")
	syscall.Mount("tmpfs", "/tmp", "tmpfs", syscall.MS_NOSUID, "mode=755")

	return nil
}
